Skip to content

Conversation

@nlemoine
Copy link
Contributor

Reasons for making this change

Fixes #4693

If this is related to existing tickets, include links to them as well. Use the syntax fixes #[issue number] (ex: fixes #123).

Checklist

  • I'm updating documentation
  • I'm adding or updating code
    • I've added and/or updated tests. I've run npx nx run-many --target=build --exclude=@rjsf/docs && npm run test:update to update snapshots, if needed.
    • I've updated docs if needed
    • I've updated the changelog with a description of the PR
  • I'm adding a new feature
    • I've updated the playground with an example use of the feature

@heath-freenome
Copy link
Member

heath-freenome commented Sep 20, 2025

@nlemoine I guess the TLDR is I'm not a fan of the approach, as it is too complex for your use case. I am open to you arguing your case for this approach over a simple flag to update the toIdSchema() generation code.

@nlemoine
Copy link
Contributor Author

Hey @heath-freenome, thanks for the thorough review! I really appreciate your time and feedback.

I understand your comments and will try to address your concerns, particularly about using toIdSchema instead of a separate path segments system. After careful consideration, I believe we need separate ID and name generation for both technical and practical reasons. Let me explain why.

Backend need examples

toIdSchema generates flat strings with consistent separators, but backends need structured notation that preserves the data hierarchy. This isn't just about changing separators or wrapping id segments within brackets - it's about preserving structural information and providing users to generate the name they might need. It may vary depending on the backend integration:

  • PHP/Rails: root[tasks][0][title] - brackets with or without array indexes
  • Django: root__tasks-0__title - different separators for objects vs arrays

The toIdSchema flag approach problem

Correct me if I'm wrong, but implementing a toIdSchema flag would result in idattributes being equals to name attributes.

And handling array indexes would also need changing code elsewhere because array indexes are not part of toIdSchema arguments:

const itemIdSchema = schemaUtils.toIdSchema(itemSchema, itemIdPrefix, itemCast, idPrefix, idSeparator);
// Compute the item UI schema using the helper method
const itemUiSchema = this.computeItemUiSchema(uiSchema, item, index, formContext);
return this.renderArrayFieldItem({
key,
index,
name: name && `${name}-${index}`,

I gave a quick and dirty try to that approach and it gave me:

<input id="root[listOfStrings]_0" name="root[listOfStrings]_0" class="form-control" label="listOfStrings-0" required="" placeholder="" type="text" aria-describedby="root[listOfStrings]_0__error root[listOfStrings]_0__description root[listOfStrings]_0__help" value="foo">

But most importantly, it would bring some more major issues, IMHO.

HTML Requires different IDs and names

Even with a toIdSchema working solution (e.g. generating bracketted ids), this isn't just about backend preferences - HTML itself requires different values for id and name attributes in several fundamental cases:

1. Radio button groups

Radio buttons in the same group must share the same name but have different id values:

<!-- ✅ Same name, different IDs -->
<input type="radio" id="root_color_0" name="root[color]" value="red">
<label for="root_color_0">Red</label>

<input type="radio" id="root_color_1" name="root[color]" value="blue">
<label for="root_color_1">Blue</label>

<!-- ❌ Using ID as name breaks radio grouping -->
<input type="radio" id="root_color_0" name="root_color_0" value="red">
<input type="radio" id="root_color_1" name="root_color_1" value="blue">

Radio group is already ok in current RJSF state but if id is used as name, this will no longer be the case.

2. Checkbox arrays

Multiple checkboxes that submit as an array need the same base name:

<!-- ✅ PHP-style array submission -->
<input type="checkbox" id="root_hobbies_0" name="root[hobbies][]" value="reading">
<input type="checkbox" id="root_hobbies_1" name="root[hobbies][]" value="gaming">
<input type="checkbox" id="root_hobbies_2" name="root[hobbies][]" value="cooking">
<!-- Submits as: hobbies = ['reading', 'gaming'] when multiple are checked -->

<!-- ❌ Using ID as name -->
<input type="checkbox" id="root_hobbies_0" name="root_hobbies_0" value="reading">
<input type="checkbox" id="root_hobbies_1" name="root_hobbies_1" value="gaming">
<!-- Submits as separate unrelated values, not an array! -->

Same as above.

3. Select multiple

Similar issue with multi-select dropdowns:

<!-- Needs array notation in name -->
<select id="root_skills" name="root[skills][]" multiple>
  <option value="js">JavaScript</option>
  <option value="python">Python</option>
</select>

Why brackets in IDs are complicated

Even if we wanted to use the same bracketed format for both ID and name (like root[tasks][0]), it would break or weaken major parts of the web stack:

CSS selectors

/* Brackets are special characters in CSS */
#root[tasks][0] { }  /* ❌ BREAKS - interpreted as attribute selector */

/* Would require escaping everywhere */
#root\[tasks\]\[0\] { }  /* Ugly and error-prone */

/* Current RJSF approach */
#root_tasks_0 { }  /* ✅ Clean, simple, works everywhere */

JavaScript DOM Queries

// querySelector fails with special characters
document.querySelector('#root[tasks][0]')  // ❌ SyntaxError

// Would need escaping
document.querySelector('#root\\[tasks\\]\\[0\\]')  // Complex escaping

// Current clean approach
document.querySelector('#root_tasks_0')  // ✅ Works everywhere

I would have loved a much simpler solution too. Adding a flag in toIdSchema feels like a workaround more than a robust and flexible alternative.

I tried to think of a one that brings the minimal possible changes. It can surely be improved (JS/React is not my strong suit) and I will address your review comments if you change your mind.

However, if you are still not convinced and don't see any benefits in bringing separate logic for id and name attributes, I would totally understand. I'm ready to work on a toIdSchema alternative, since I'd really like that feature to make it for version 6.

What do you think? I'll be happy to refactor the implementation based on your preferences.

@heath-freenome
Copy link
Member

heath-freenome commented Sep 30, 2025

@nlemoine Thank you for the well documented explanation. It has convinced me and my compatriot @nickgros that there is merit in your approach. So much so, that I've just pushed a PR that does a major breaking refactor of the system to simplify your work quite a bit.

In order to implement your feature, I am going to recommend the following approach after my PR merges:

  1. Add the nameGenerator to the GlobalFormOptions as optional
  2. Update the FieldPathId to add an optional name variable
  3. Update the toFieldPathId() function to generate the name if the nameGenerator exists (you can check the type of the element in the path prop to determine object or array)
  4. Update the WidgetProps to add the optional htmlName prop
  5. Update everywhere the <Widget ...> is used in the codebase to add htmlName={fieldPathId.name}
  6. Update all of the widget implementations in the codebase to add htmlName || id for the name props
  7. Update all of the tests to deal with that change
  8. Update the docs to add the optional htmlName to the Widget docs
  9. Update the v6.x migration guide to describe this new feature as well as let custom widget developers know about the htmlName props

@nlemoine
Copy link
Contributor Author

nlemoine commented Oct 3, 2025

HI @heath-freenome,

Thank you so much for paying so much attention to that issue and bringing so much changes to that purpose! 🙌

I'll try to update the PR as soon as I can and will keep posted.

@nlemoine nlemoine force-pushed the feat-name-generator branch 2 times, most recently from c921892 to 621a1fa Compare October 6, 2025 19:29
@nlemoine
Copy link
Contributor Author

nlemoine commented Oct 6, 2025

Quick questions @heath-freenome

  • Do I have to update every RJSF theme?

  • I'm having an issue the current implementation doesn't solve. When displaying array as checkboxes, there is no index and the elementType is still an object. So no bracket is generated (e.g. root[multipleChoicesList][]). I currently don't have the information that tells me if I should use brackets or not. Do you have any idea for that use case?

  • I noticed that you've provided some sample implementations of these in utils. If someone wanted to just use them straight up, but with a different rootName they could wrap them?

    Yes they could but say you want to output multiple forms on the same page and want different root names, you will have to create multiple wrapper functions to get this done, which is quite ugly. However, I would be fine reusing the idPrefix as a parameter of the name generator function to provide a custom root name. Imagine the same logic for Ids, wrapping toIdSchema for each customization would be cumbersome.

  • Do you want me to remove initially provided nameGenerator functions (in utils, bracket/dot notation you mentioned in the quote above)? I just committed them to enable an easier playground preview but I would be fine and they're not part of RJSF.

@heath-freenome
Copy link
Member

heath-freenome commented Oct 6, 2025

Quick questions @heath-freenome

  • Do I have to update every RJSF theme?
    Yes, every theme will need updating, sorry. New features need to work across all published themes
  • I'm having an issue the current implementation doesn't solve. When displaying array as checkboxes, there is no index and the elementType is still an object. So no bracket is generated (e.g. root[multipleChoicesList][]). I currently don't have the information that tells me if I should use brackets or not. Do you have any idea for that use case?

Right now the optionId() utility function in the CheckboxesWidget implementations are adding the -{index#} notations. Those are ids, not names. The name should be the same for all of the checkboxes, so simply passing htmlName | id still works fine.

  • I noticed that you've provided some sample implementations of these in utils. If someone wanted to just use them straight up, but with a different rootName they could wrap them?
    Or you could make the name generator take the idPrefix as an optional parameter.

    Yes they could but say you want to output multiple forms on the same page and want different root names, you will have to create multiple wrapper functions to get this done, which is quite ugly. However, I would be fine reusing the idPrefix as a parameter of the name generator function to provide a custom root name.
    Right now the only solution people have to this anyway is to render two different Form elements passing different idPrefix values, so let's just stick with that

  • Do you want me to remove initially provided nameGenerator functions (in utils, bracket/dot notation you mentioned in the quote above)? I just committed them to enable an easier playground preview but I would be fine and they're not part of RJSF.

I'm fine with NOT providing them. You will still need something to test with so perhaps adding them is helpful?

@nlemoine
Copy link
Contributor Author

nlemoine commented Oct 6, 2025

Right now the optionId() utility function in the CheckboxesWidget implementations are adding the -{index#} notations. Those are ids, not names. The name should be the same for all of the checkboxes, so simply passing htmlName | id still works fine.

Actually, no, multiple checkboxes are considered an array. So they need brackets (root[multipleChoicesList][] or root[multipleChoicesList][0]).

The name is only generated in nameGenerator and in the case I showed you, the path arg only contains ['multipleChoicesList']. With that single arg, I have no way to tell (inside nameGenerator) if I should add brackets or not. Once in widget, it's "too late" to change the name (no optionId way). That's why my first implementation added indexes as an arg of the name generator.

@heath-freenome
Copy link
Member

Right now the optionId() utility function in the CheckboxesWidget implementations are adding the -{index#} notations. Those are ids, not names. The name should be the same for all of the checkboxes, so simply passing htmlName | id still works fine.

Actually, no, multiple checkboxes are considered an array. So they need brackets (root[multipleChoicesList][] or root[multipleChoicesList][0]).

The name is only generated in nameGenerator and in the case I showed you, the path arg only contains ['multipleChoicesList']. With that single arg, I have no way to tell (inside nameGenerator) if I should add brackets or not. Once in widget, it's "too late" to change the name (no optionId way). That's why my first implementation added indexes as an arg of the name generator.

I'm not sure I understand why the indexes are needed for the name of checkboxes in a checkbox group? Since submitting the form will automatically cause the html to return name=X&name=Y&name=Z to the submitted form, which, using standard HTML parsing should result in name=[X,Y,Z]

@nlemoine
Copy link
Contributor Author

nlemoine commented Oct 7, 2025

I'm not sure I understand why the indexes are needed for the name of checkboxes in a checkbox group?

In PHP at least, this makes a huge difference when getting posted data (same name VS same name[]):

Capture d’écran 2025-10-07 à 21 19 12 Capture d’écran 2025-10-07 à 21 19 39

Long story short: when field(s) posted values are an array, they must use brackets (set of checkboxes, select with multiple attribute).

@heath-freenome
Copy link
Member

heath-freenome commented Oct 7, 2025

I'm not sure I understand why the indexes are needed for the name of checkboxes in a checkbox group?
...
Long story short: when field(s) posted values are an array, they must use brackets (set of checkboxes, select with multiple attribute).

My suggestion would be to create a new utility function in @rjsf/utils called toOptionName<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(id: string, htmlName: string | undefined, registry: Registry<T, S, F>, index: number): string function that uses the nameGenerator to append the [index] onto the end of the htmlName as needed for the index, when htmlName and nameGenerator exist, otherwise simply return htmlName | id. Then update all the widgets that need it to use it for the name.

@heath-freenome
Copy link
Member

@nlemoine Will you have this feature completed sometime in the next 10 days? I'd like to get it into a prerelease of v6 before we go live with the official release

@nlemoine nlemoine force-pushed the feat-name-generator branch 2 times, most recently from 85e8c6f to 69fe928 Compare October 14, 2025 19:38
@nlemoine
Copy link
Contributor Author

nlemoine commented Oct 14, 2025

Hi @heath-freenome,

Sorry for the delay - I had a busy week with limited time to work on this.

I've finally managed to put everything together. I couldn't go with the solution you initially suggested. The key insight is that for multiple <input type="checkbox" name="root[hobbies][]" /> or <select multiple name="root[hobbies][]">, what matters is knowing whether we're handling multiple values rather than the specific index. A <select multiple> is a single field tag that handles multiple values, while checkboxes are multiple input tags that also handle multiple values - but both need the same [] suffix in bracket notation.

This is why I added an optional isMultiValue parameter to NameGeneratorFunction and toFieldPathId. This approach:

  • Works for both checkboxes and multi-select dropdowns
  • Lets the name generator decide how to handle multi-value fields (PHP uses [], but other backends might differ)
  • Keeps the solution flexible and not hardcoded to any specific notation

Can you review this approach before I propagate these changes to all the theme libraries? I'd like your approval first before going further.

Thanks!

Copy link
Member

@heath-freenome heath-freenome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nlemoine I like that approach as well. Go ahead and do the bigger update. Don't forget the docs and the CHANGELOG.md as well

@nlemoine nlemoine force-pushed the feat-name-generator branch from 6665a60 to 78f0294 Compare October 15, 2025 19:17
@nlemoine nlemoine force-pushed the feat-name-generator branch from e501a38 to a67c3e1 Compare October 17, 2025 05:49
@nlemoine nlemoine marked this pull request as ready for review October 17, 2025 05:57
Copy link
Member

@heath-freenome heath-freenome left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a few suggested changes. I also have a PR to convert the ArrayField class component into a set of stateless functional components. If mine merges first, you will likely have to rework your ArrayField changes in that file. If you can get the changes made in the next few hours, then I will merge yours and update mine instead

const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);

// For custom widgets with multiple=true, generate a fieldPathId with isMultiValue flag
const multiValueFieldPathId = toFieldPathId('', globalFormOptions, fieldPathId, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent rerender due to new fieldPathId that is exactly the same (but different storage), can you import the new useDeepCompareMemo function from @rjsf/utils and do this?

Suggested change
const multiValueFieldPathId = toFieldPathId('', globalFormOptions, fieldPathId, true);
const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));

Copy link
Contributor Author

@nlemoine nlemoine Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what I should do about this. I can't use that React hook here in the current state of my PR.

React Hook "useDeepCompareMemo" cannot be called in a class component. React Hooks must be called in a React function component or a custom React Hook

Copy link
Member

@heath-freenome heath-freenome Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll merge yours then update it in mine

const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);

// For multi-select widgets, generate a fieldPathId with isMultiValue flag
const multiValueFieldPathId = toFieldPathId('', globalFormOptions, fieldPathId, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent rerender due to new fieldPathId that is exactly the same (but different storage), can you import the new useDeepCompareMemo function from @rjsf/utils and do this?

Suggested change
const multiValueFieldPathId = toFieldPathId('', globalFormOptions, fieldPathId, true);
const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));

const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);

// For file widgets with multiple=true, generate a fieldPathId with isMultiValue flag
const multiValueFieldPathId = toFieldPathId('', globalFormOptions, fieldPathId, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent rerender due to new fieldPathId that is exactly the same (but different storage), can you import the new useDeepCompareMemo function from @rjsf/utils and do this?

Suggested change
const multiValueFieldPathId = toFieldPathId('', globalFormOptions, fieldPathId, true);
const multiValueFieldPathId = useDeepCompareMemo(toFieldPathId('', globalFormOptions, fieldPathId, true));

Authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com>
@nlemoine nlemoine force-pushed the feat-name-generator branch from 1ff0b25 to a318c15 Compare October 17, 2025 19:50
@nlemoine
Copy link
Contributor Author

nlemoine commented Oct 17, 2025

I made some of the changes you suggested but didn't include the useDeepCompareMemo hooks. See my comment in your review.

This is unrelated but may I ask if there's a recommended folder structure for themes?

I noticed some of them follow this structure:

├── components
│   ├── fields
│   ├── templates
│   │   ├── ButtonTemplates
│   │   ├── FieldTemplate
│   └── widgets

While others are structured like:

├── AddButton
├── ArrayFieldItemTemplate
├── ArrayFieldTemplate
├── BaseInputTemplate
├── CheckboxesWidget
├── CheckboxWidget
├── DescriptionField
├── ErrorList
├── FieldErrorTemplate
├── FieldHelpTemplate
├── FieldTemplate
├── Form
├── GridTemplate
├── IconButton
├── MultiSchemaFieldTemplate
├── ObjectFieldTemplate
├── OptionalDataControlsTemplate
├── RadioWidget
├── RangeWidget
├── SelectWidget
├── SubmitButton
├── Templates
├── TextareaWidget
├── Theme
├── TitleField
├── Widgets
└── WrapIfAdditionalTemplate

Any advice for building a custom theme?

@heath-freenome
Copy link
Member

I made some of the changes you suggested but didn't include the useDeepCompareMemo hooks. See my comment in your review.

This is unrelated but may I ask if there's a recommended folder structure for themes?

I noticed some of them follow this structure:

├── components
│   ├── fields
│   ├── templates
│   │   ├── ButtonTemplates
│   │   ├── FieldTemplate
│   └── widgets

While others are structured like:

├── AddButton
├── ArrayFieldItemTemplate
├── ArrayFieldTemplate
├── BaseInputTemplate
├── CheckboxesWidget
├── CheckboxWidget
├── DescriptionField
├── ErrorList
├── FieldErrorTemplate
├── FieldHelpTemplate
├── FieldTemplate
├── Form
├── GridTemplate
├── IconButton
├── MultiSchemaFieldTemplate
├── ObjectFieldTemplate
├── OptionalDataControlsTemplate
├── RadioWidget
├── RangeWidget
├── SelectWidget
├── SubmitButton
├── Templates
├── TextareaWidget
├── Theme
├── TitleField
├── Widgets
└── WrapIfAdditionalTemplate

Any advice for building a custom theme?

I'd go with the second pattern... At some point I'll get around to making all the themes have the same directory structure.

@heath-freenome heath-freenome merged commit 148f72f into rjsf-team:main Oct 17, 2025
4 checks passed
@heath-freenome
Copy link
Member

heath-freenome commented Oct 17, 2025

@nlemoine Even though I merged this, I just realized that you don't actually have any tests that verify adding a name generator does what you expect. Can you, at minimum, add some snapshot tests that show off the name generators in action?

You would simply have to update the arrayTests.tsx, objectTests.tsx and formTests.tsx in the snapshot-tests package, build it, then run through each theme and run npm run test:update to update the tests in each theme. Then verify the snapshots added the names as you would expect... Thanks!

@nlemoine nlemoine deleted the feat-name-generator branch October 18, 2025 08:40
@nlemoine nlemoine mentioned this pull request Oct 18, 2025
8 tasks
@nlemoine
Copy link
Contributor Author

Hi @heath-freenome,

Thanks for merging this!

Sorry, I indeed forgot to add some tests. You will extensive tests in #4809

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compliant form elements names for "old" submitted data (POST)

2 participants